package dom.xpath.data;

import java.io.IOException;
import dom.xpath.core.Prop;
import dom.xpath.io.PrintOutput;
import dom.xpath.util.Token;
import dom.xpath.util.TokenBuilder;
import dom.xpath.util.TokenList;
import org.xml.sax.SAXException;
import static dom.xpath.data.DataText.*;

/**
 * This class provides serialization methods for the database contents.
 *
 * @author Workgroup DBIS, University of Konstanz 2005-07, ISC License
 * @author Christian Gruen
 */
public final class Serialize {
  /** Private constructor, preventing class instantiation. */
  private Serialize() { }

  /**
   * Prints the text declaration to the output stream.
   * @param out output stream
   * @throws IOException in case of problems with the PrintOutput
   */
  public static void decl(final PrintOutput out) throws IOException {
    // <CG> XML/Serialize: convert back to original encoding
    //println("<?xml version='1.0' encoding='" + data.meta.encoding + "' ?>");
    out.println("<?xml version='1.0' encoding='" + Token.UTF8 + "' ?>");
  }
  
  /**
   * Prints the specified node set to the output stream.
   * @param out output stream
   * @param data data reference
   * @param valid valid flag
   * @param nodes node set
   * @throws IOException in case of problems with the PrintOutput
   */
  public static void xml(final PrintOutput out, final Data data,
      final Nodes nodes, final boolean valid) throws IOException {

    final int size = nodes.size;
    if(size == 0) {
      if(valid) out.println(RESULTSEMPTY);
      return;
    }
    
    if(valid) {
      decl(out);
      out.println(RESULTSOPEN);
    }
    for(int c = 0; c < size; c++) {
      if(out.finished()) break;
      
      xml(out, data, nodes.pre[c], valid, data.meta.entity);
      if(!valid && c != size - 1) out.print(",");
      out.print(Prop.NL);
    }
    if(valid) out.println(RESULTSCLOSE);
  }
  
  /**
   * Prints the specified node to the output stream.
   * @param out output stream
   * @param data data reference
   * @param val valid flag
   * @param pre table position
   * @param ent dissolve entities
   * @return last p value
   * @throws IOException in case of problems with the PrintOutput
   */
  public static int xml(final PrintOutput out, final Data data,
      final int pre, final boolean val, final boolean ent) throws IOException {
    
    int p = pre;
    final int kind = data.kind(p);
    if(kind == Data.TEXT) {
      out.print(val ? RESULTOPEN : TXT1);
      out.printToken(data.text(p), ent);
      out.print(val ? RESULTCLOSE : TXT2);
    } else if(kind == Data.ATTR) {
      if(val) out.print(RESULTATT1);
      out.print(data.attName(p));
      out.print(val ? RESULTATT2 : ATT2);
      out.printToken(data.attValue(p), ent);
      out.print(val ? RESULTATT3 : ATT3);
    } else if(kind == Data.DOC) {
      if(val) out.print(RESULTOPEN);
      xml(out, data, p + 1);
      if(val) out.print(RESULTCLOSE);
    } else if(kind == Data.COMM) {
      if(val) out.print(RESULTOPEN);
      xml(out, COM1, data.text(p), COM2);
      if(val) out.print(RESULTCLOSE);
    } else if(kind == Data.PI) {
      if(val) out.print(RESULTOPEN);
      xml(out, PI1, data.text(p), PI2);
      if(val) out.print(RESULTCLOSE);
    } else {
      // write element node
      if(val) out.print(RESULTOPEN);
      p = xml(out, data, p);
      if(val) out.print(RESULTCLOSE);
    }
    return p;
  }
  
  /**
   * Writes a node.
   * @param out output stream
   * @param pref prefix
   * @param suff suffix
   * @param val value to be written
   * @throws IOException in case of problems with the PrintOutput
   */
  public static void xml(final PrintOutput out, final byte[] pref,
      final byte[] val, final byte[] suff) throws IOException {
    out.print(pref);
    out.print(val);
    out.print(suff);
  }
  
  /**
   * Writes the specified XML node and all its sub nodes to the specified
   * output stream.
   * @param out writer instance
   * @param data data reference
   * @param pos XML node to be written
   * @return last p value
   * @throws IOException in case of problems with the PrintOutput
   */
  public static int xml(final PrintOutput out, final Data data,
      final int pos) throws IOException {

    // stacks
    final int[] parent = new int[256];
    final byte[][] token = new byte[256][];
    // current indentation flag
    boolean indent = false;
    // current output level
    int l = 0;
    int p = pos;
    final boolean format = data.meta.chop;

    // start with the root node
    final int root = p;

    // loop through all table entries
    final int s = data.size;
    while(p < s) {
      if(out.finished()) return s;

      final int kind = data.kind(p);
      final int par = data.parent(p, kind);
      // skip writing if all sub nodes were processed
      if(root != 1 && p > root && par < root) break;
      
      // close opened tags...
      while(l > 0) {
        if(parent[l - 1] < par) break;
        if(indent) indent(out, l - 1);
        out.print(ELEM3);
        out.print(token[--l]);
        out.print(ELEM2);
        indent = format;
      }

      if(kind == Data.TEXT) {
        out.printToken(data.text(p++), data.meta.entity);
        indent = false;
      } else if(kind == Data.COMM) {
        if(indent) indent(out, l);
        xml(out, COM1, data.text(p++), COM2);
        indent = format;
      } else if(kind == Data.PI) {
        if(indent) indent(out, l);
        xml(out, PI1, data.text(p++), PI2);
        indent = format;
      } else {
        // add element node
        if(indent) indent(out, l);
        out.print(ELEM1);
        final byte[] tok = data.tag(p);
        out.print(tok);

        // add element attributes
        final int ps = p + data.size(p, kind);
        final int as = p + data.attSize(p, kind);
        while(++p != as) {
          out.print(' ');
          out.print(data.attName(p));
          out.print(ATT2);
          out.printToken(data.attValue(p), data.meta.entity);
          out.print(ATT3);
        }

        // check if this is an empty tag
        if(p == ps) {
          out.print('/');
        } else {
          token[l] = tok;
          parent[l++] = par;
        }
        out.print(ELEM2);
        indent = format;
      }
    }
    // process nodes that remain in the stack
    while(l > 0) {
      if(indent) indent(out, l - 1);
      out.print(ELEM3);
      out.print(token[--l]);
      out.print(ELEM2);
      indent = format;
    }
    return p;
  }
  
  /**
   * Prints the specified node set to the output stream.
   * @param rr default handler
   * @param data data reference
   * @param nodes node set
   * @throws SAXException SAX exception
   */
  public static void xml(final ResultReader rr, final Data data,
      final Nodes nodes) throws SAXException {

    rr.startElement(Token.string(RESULTS));
    for(int c = 0; c < nodes.size; c++) Serialize.xml(rr, data, nodes.pre[c]);
    rr.endElement(Token.string(RESULTS));
  }
  
  /**
   * Writes the specified XML node and all its sub nodes to the specified
   * output stream.
   * @param rr default handler
   * @param data data reference
   * @param pos XML node to be written
   * @throws SAXException SAX exception
   */
  public static void xml(final ResultReader rr, final Data data,
      final int pos) throws SAXException {

    // stacks
    final int[] parent = new int[256];
    final String[] token = new String[256];
    // current output level
    int l = 0;
    int p = pos;

    // start with the root node
    final int root = p;

    // loop through all table entries
    final int s = data.size;
    while(p < s) {
      final int kind = data.kind(p);
      final int par = data.parent(p, kind);
      // skip writing if all sub nodes were processed
      if(root != 1 && p > root && par < root) break;
      
      // close opened tags...
      while(l > 0) {
        if(parent[l - 1] < par) break;
        rr.endElement(token[--l]);
      }

      if(kind == Data.TEXT) {
        rr.text(data.text(p++));
      } else if(kind == Data.COMM) {
        // data.text(p++) ?
      } else if(kind == Data.PI) {
        rr.pi(Token.EMPTY, data.text(p++));
      } else {
        // add element node
        final String tok = Token.string(data.tag(p));

        // add element attributes
        final TokenList tl = new TokenList();
        final int ps = p + data.size(p, kind);
        final int as = p + data.attSize(p, kind);
        while(++p != as) {
          tl.add(data.attName(p));
          tl.add(data.attValue(p));
        }
        rr.startElement(tok, tl);

        // check if this is an empty tag
        if(p == ps) {
          rr.endElement(tok);
        } else {
          token[l] = tok;
          parent[l++] = par;
        }
      }
    }
    // process nodes that remain in the stack
    while(l > 0) rr.endElement(token[--l]);
  }

  /**
   * Adds some whitespace indentation.
   * @param out writer instance
   * @param lvl current level depth
   * @throws IOException in case of problems with the PrintOutput
   */
  private static void indent(final PrintOutput out, final int lvl) 
      throws IOException {
    out.print(Prop.NL);
    for(int i = 0; i < (lvl << 1); i++) out.print(' ');
  }
  
  /**
   * Writes the database as table.
   * @param out output stream
   * @param data data reference
   * @param nodes node set
   * @throws IOException build or write error
   */
  public static void table(final PrintOutput out, final Data data,
      final Nodes nodes) throws IOException {

    // dump the db table
    tableHeader(out, data);
    for(int i = 0; i < nodes.size; i++) table(out, data, nodes.pre[i]);
  }
  
  /**
   * Writes the database as table.
   * @param out output stream
   * @param data data reference
   * @param s first node to be written
   * @param e last node to be written
   * @throws IOException build or write error
   */
  public static void table(final PrintOutput out, final Data data,
      final int s, final int e) throws IOException {

    // dump the db table
    tableHeader(out, data);
    for(int p = s; p < e; p++) table(out, data, p);
  }

  /**
   * Writes the header for the 'table' command.
   * @param out output stream
   * @param data data reference
   * @throws IOException build or write error
   */
  private static void tableHeader(final PrintOutput out, final Data data)
      throws IOException {
    // write table header
    final int len = Token.numDigits(data.size);
    format(out, Token.token(TABLEHEAD1), len + 1);
    format(out, Token.token(TABLEHEAD2), len + 2);
    format(out, Token.token(TABLEHEAD3), len + 2);
    format(out, Token.token(TABLEHEAD4), 4);
    out.print(TABLEHEAD5);
  }

  /**
   * Writes the table representation of the database to the specified output
   * stream.
   * @param out output stream
   * @param data data reference
   * @param p node to be printed
   * @throws IOException build or write error
   */
  private static void table(final PrintOutput out, final Data data,
      final int p) throws IOException {
    final int len = Token.numDigits(data.size);
    format(out, p, len + 1);
    final int k = data.kind(p);
    format(out, p - data.parent(p, k), len + 2);
    format(out, data.size(p, k), len + 2);
    format(out, data.attSize(p, k), 4);

    if(k == Data.ELEM) out.print(TABLEELEM);
    else if(k == Data.DOC) out.print(TABLEDOC);
    else if(k == Data.TEXT) out.print(TABLETEXT);
    else if(k == Data.COMM) out.print(TABLECOMM);
    else if(k == Data.ATTR) out.print(TABLEATTR);
    else if(k == Data.PI) out.print(TABLEPI);

    out.print("  ");
    out.print(k == Data.TEXT || k == Data.COMM || k == Data.PI ? data.text(p) :
      k == Data.ATTR ? data.attName(p) : data.tag(p));

    if(k == Data.ATTR) {
      out.print(ATT2);
      out.print(data.attValue(p));
      out.print(ATT3);
    }
    out.print(Prop.NL);
  }
  
  /**
   * Convenience method, returning a string representation of the current node.
   * @param data data reference
   * @param p pre value
   * @param s short representation
   * @return string representation
   */
  public static byte[] content(final Data data, final int p, final boolean s) {
    final int kind = data.kind(p);
    if(kind == Data.ELEM || kind == Data.DOC) {
      return data.tag(p);
    } else if(kind == Data.TEXT) {
      return s ? TEXT : data.text(p);
    } else if(kind == Data.COMM) {
      return s ? COMM : Token.append(COM1, Token.append(data.text(p), COM2));
    } else if(kind == Data.PI) {
      return s ? PI : Token.append(PI1, Token.append(data.text(p), PI2));
    }
    final TokenBuilder tb = new TokenBuilder();
    tb.add(ATT1);
    tb.add(data.attName(p));
    if(!s) {
      tb.add(ATT2);
      tb.add(data.attValue(p));
      tb.add(ATT3);
    }
    return tb.finish();
  }
  
  /**
   * Convenience method, returning a string representation of the current node.
   * @param data data reference
   * @param p pre value
   * @return string representation
   */
  public static String node(final Data data, final int p) {
    final TokenBuilder tb = new TokenBuilder();
    final int kind = data.kind(p);
    if(kind == Data.TEXT) {
      tb.add(TXT1);
      tb.add(data.text(p));
      tb.add(TXT2);
    } else if(kind == Data.ATTR) {
      tb.add(data.attName(p));
      tb.add(ATT2);
      tb.add(data.attValue(p));
      tb.add(ATT3);
    } else if(kind == Data.DOC) {
      tb.add(data.tag(p));
    } else if(kind == Data.COMM) {
      tb.add(COM1);
      tb.add(data.text(p));
      tb.add(COM2);
    } else if(kind == Data.PI) {
      tb.add(PI1);
      tb.add(data.text(p));
      tb.add(PI2);
    } else {
      tb.add(ELEM1);
      tb.add(data.tag(p));
      tb.add(ELEM2);
    }
    return tb.toString();
  }

  /**
   * Formats an integer value for the table output.
   * @param out output stream
   * @param n number to be formatted
   * @param left size of the table entry
   * @throws IOException build or write error
   */
  private static void format(final PrintOutput out, final int n,
      final int left) throws IOException {
    format(out, Token.token(n), left);
  }

  /**
   * Formats an output string for the table output.
   * @param out output stream
   * @param t text to be formatted
   * @param s size of the table entry
   * @throws IOException build or write error
   */
  private static void format(final PrintOutput out, final byte[] t,
      final int s) throws IOException {
    int l = s - t.length;
    while(l-- > 0) out.print(' ');
    out.print(t);
  }
}
